-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix one-tick-race condition in asyncMap #11249
Conversation
🦋 Changeset detectedLatest commit: 591d6c1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
size-limit report 📦
|
}, | ||
(error) => { | ||
--activeCallbackCount; | ||
throw error; | ||
} | ||
) | ||
.catch((caught) => { | ||
error && error.call(observer, caught); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The race condition essentially is the following:
handler.next
(line 58) is called by the upstream Observable- code starts flowing in
.then(both, both)
,both
(the mapping function) throws an error (as it should in this specific scenario, because theresult
from the server containserrors
andboth
is the function to handle that) - code reaches the
--activeCallbackCount
in line 43 and waits a tick before executingerror.call(observer, caught)
- meanwhile,
handler.complete
is called by the upstream Observable activeCallbackCount
is currently 0,complete.call(observer)
executes⚠️ now it doesn't matter anymore iferror.call(observer, caught)
executes or not - the downstream observable is already complete (in our case without data, since we went into the error case, not the success case, sonext
was never called).
promiseQueue.finally(() => { | ||
--activeCallbackCount; | ||
if (completed) { | ||
handler.complete!(); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To fix this, --activeCallbackCount
has to be executed after next.call(observer, result)
/error.call(observer, caught)
above had a chance to execute.
I moved this out of the promise chain that is assigned to promiseQueue
, as we don't need to queue up another tick in case promiseQueue
is reused within a very short time span.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this logic, but I'm sure our future selves would appreciate a comment about why the .finally
isn't just chained after the .catch
.
/release:pr |
A new release has been made for this PR. You can install it with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I support this general solution, and I appreciate your careful reasoning, but have one question (and a couple suggestions) for you to consider.
promiseQueue.finally(() => { | ||
--activeCallbackCount; | ||
if (completed) { | ||
handler.complete!(); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this logic, but I'm sure our future selves would appreciate a comment about why the .finally
isn't just chained after the .catch
.
promiseQueue.finally(() => { | ||
--activeCallbackCount; | ||
if (completed) { | ||
handler.complete!(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
promiseQueue.finally(() => { | |
--activeCallbackCount; | |
if (completed) { | |
handler.complete!(); | |
promiseQueue.finally(() => { | |
--activeCallbackCount; | |
// Because of the .finally delay, it's possible handler.complete() gets | |
// called shortly before activeCallbackCount falls to 0, so each time we | |
// decrement activeCallbackCount we give handler.complete() another | |
// chance to call observer.complete(). | |
if (completed) { | |
handler.complete!(); |
This might be a helpful comment, given the racy nature of this code.
.then( | ||
(result) => { | ||
--activeCallbackCount; | ||
next && next.call(observer, result); | ||
if (completed) { | ||
handler.complete!(); | ||
} | ||
}, | ||
(error) => { | ||
--activeCallbackCount; | ||
throw error; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were not previously calling handler.complete()
in the error handler, but now the promiseQueue.finally
callback is potentially calling handler.complete()
whenever we decrement activeCallbackCount
. Is that a problem? Specifically, is it possible we might call error.call(observer, caught)
and then also call complete.call(observer)
?
Fixes #11149
(hopefully 🤞 )
Can be tried out in https://github.com/quocluongha/rn-apollo-test - I can't reproduce it in the browser. It seems this specific timing only triggers in React Native.
I'll try to explain this one in comments...
Checklist:
(This seems impossible to test for, but trying out in the linked repo it seems to fix the bug)